﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Diagnostics;
using System.Linq;
using System.Web.Mvc;

using Lilium.Reflection;

namespace Lilium.Web.Mvc.Metadata
{
	/// <summary>
	/// 
	/// </summary>
	/// <remarks>
	/// Thread Safety:
	/// This type is thread safe.
	/// </remarks>
	public sealed class MappingSourceMetadataProvider : IEntityMetadataProvider
	{
		public MappingSourceMetadataProvider(MappingSource mappingSource, Type dataContextType)
		{
			if (mappingSource == null) throw new ArgumentNullException("mappingSource");
			if (dataContextType == null) throw new ArgumentNullException("dataContextType");

			m_DataContextType = dataContextType;
			m_MappingSource = mappingSource;
		}

		#region IEntityMetadataProvider Members

		public void ExtendMetadata(ModelMetadata modelMetadata, IEnumerable<Attribute> attributes)
		{
			if (modelMetadata == null) throw new ArgumentNullException("modelMetadata");
			if (attributes == null) throw new ArgumentNullException("attributes");

			IEnumerable<MetadataExtensionDescriptor> extensionDescriptors;

			lock (MetadataCache)
			{
				var key = Tuple.Create(modelMetadata.ModelType, modelMetadata.ContainerType, modelMetadata.PropertyName);
				if (!MetadataCache.TryGetValue(key, out extensionDescriptors))
				{
					var model = MappingSource.GetModel(DataContextType);
					var metaType = model.GetMetaType(modelMetadata.ModelType);

					var extensionDescriptorList = new List<MetadataExtensionDescriptor>();

					extensionDescriptorList.Add(new MetadataExtensionDescriptor(
						new EntityMetadataExtension(
							new EntityKeyDescription(metaType.DataMembers.Where(m => m.IsPrimaryKey).OrderBy(m => m.Ordinal).Select(m => m.Name))),
						typeof(EntityMetadataExtension),
						null));

					foreach (var displayAttribute in attributes.OfType<DisplayAttribute>())
						extensionDescriptorList.Add(new MetadataExtensionDescriptor(
							new DisplayMetadataExtension(
								displayAttribute.Scaffold,
								displayAttribute.TemplateName,
								displayAttribute.HideSurroundingChrome),
							typeof(DisplayMetadataExtension),
							displayAttribute.ViewName));

					if (!string.IsNullOrEmpty(modelMetadata.PropertyName))
					{
						if ("BusinessProcessId" == modelMetadata.PropertyName)
						{
						}

						var entityModel = model.GetMetaType(modelMetadata.ContainerType);

						var association = entityModel.Associations.FirstOrDefault(a => 
						{
							var res = a.ThisMember.Member.Name == modelMetadata.PropertyName
                                || ((a.OtherMember != null) && (a.OtherMember.Member.Name == modelMetadata.PropertyName))
								|| a.ThisKey[0].Name == modelMetadata.PropertyName
								|| a.OtherKey[0].Name == modelMetadata.PropertyName;
								
							return res;
						});

						if (association != null)
						{
							var referencedType = association.IsForeignKey ? association.OtherType : metaType;

							var associationMetadataExtension = new AssociationMetadataExtension(
								referencedType.Type,
								association.IsForeignKey,
								association.ThisKey[0].Name,
								association.OtherKey[0].Name,
								referencedType.Type.GetCustomAttributes<DisplayColumnAttribute>().Select(a => a.DisplayColumn).FirstOrDefault(),
								referencedType.Type.GetCustomAttributes<DisplayColumnAttribute>().Select(a => a.SortColumn).FirstOrDefault());

							extensionDescriptorList.Add(new MetadataExtensionDescriptor(associationMetadataExtension, typeof(AssociationMetadataExtension), null));
						}
					}

					extensionDescriptors = extensionDescriptorList.ToArray();
					MetadataCache.Add(key, extensionDescriptors);
				}
			}

			foreach (var extensionDescriptor in extensionDescriptors) // enumerating is thread-safe
				modelMetadata.AddExtension(extensionDescriptor.Extension, extensionDescriptor.Type, extensionDescriptor.Name);
		}

		#endregion

		private static Type GetReferencedType(Type rawType)
		{
			if (rawType == null) throw new ArgumentNullException("rawType");

			if (rawType.IsGenericType && typeof(EntitySet<>).IsAssignableFrom(rawType.GetGenericTypeDefinition()))
			{
				return rawType.GetGenericArguments()[0];
			}
			return rawType;
		}

		#region private Type DataContextType

		private readonly Type m_DataContextType;

		private Type DataContextType
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_DataContextType;
			}
		}

		#endregion

		#region private MappingSource MappingSource

		private readonly MappingSource m_MappingSource;

		private MappingSource MappingSource
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_MappingSource;
			}
		}

		#endregion

		#region private Dictionary<Tuple<Type, string>, EntityMetadataExtension> MetadataCache

		private readonly Dictionary<Tuple<Type, Type, string>, IEnumerable<MetadataExtensionDescriptor>> m_MetadataCache = new Dictionary<Tuple<Type, Type, string>, IEnumerable<MetadataExtensionDescriptor>>();

		private Dictionary<Tuple<Type, Type, string>, IEnumerable<MetadataExtensionDescriptor>> MetadataCache
		{
			get
			{
				return m_MetadataCache;
			}
		}

		#endregion

		#region private class MetadataExtensionDescriptor

		private class MetadataExtensionDescriptor
		{
			public MetadataExtensionDescriptor(object extension, Type type, string name)
			{
				m_Extension = extension;
				m_Name = name;
				m_Type = type;
			}

			#region public object Extension

			private readonly object m_Extension;

			public object Extension
			{
				get
				{
					return m_Extension;
				}
			}

			#endregion

			#region public string Name

			private readonly string m_Name;

			public string Name
			{
				get
				{
					return m_Name;
				}
			}

			#endregion

			#region public Type Type

			private readonly Type m_Type;

			public Type Type
			{
				get { return m_Type; }
			}

			#endregion
		}

		#endregion
	}
}